Poznaj alternatywy dla enumów TypeScript, np. asercje stałych i typy unijne. Odkryj ich zalety, wady i praktyczne zastosowania dla czystszego kodu globalnie.
Alternatywy dla enumów w TypeScript: Asercje stałych i typy unijne dla solidnego kodu
TypeScript, potężny nadzbiór JavaScriptu, wprowadza statyczne typowanie do dynamicznego świata tworzenia stron internetowych. Wśród wielu jego funkcji, słowo kluczowe enum od dawna jest podstawą do definiowania zestawu nazwanych stałych. Enumy zapewniają przejrzysty sposób reprezentowania stałej kolekcji powiązanych wartości, zwiększając czytelność i bezpieczeństwo typów.
Jednakże, w miarę dojrzewania ekosystemu TypeScript i wzrostu złożoności oraz skali projektów, deweloperzy na całym świecie coraz częściej kwestionują tradycyjną użyteczność enumów. Choć są proste w przypadku nieskomplikowanych zastosowań, enumy wprowadzają pewne zachowania i charakterystyki środowiska wykonawczego, które czasami mogą prowadzić do nieoczekiwanych problemów, wpływać na rozmiar pakietu lub komplikować optymalizacje tree-shakingu. Doprowadziło to do szeroko zakrojonych poszukiwań alternatyw.
Ten obszerny przewodnik zagłębia się w dwie prominentne i wysoce skuteczne alternatywy dla enumów TypeScript: Typy unijne z literałami string/numerycznymi oraz Asercje stałych (as const). Zbadamy ich mechanizmy, praktyczne zastosowania, korzyści i kompromisy, dostarczając wiedzy do podejmowania świadomych decyzji projektowych dla Twoich projektów, niezależnie od ich rozmiaru czy globalnego zespołu nad nimi pracującego. Naszym celem jest umożliwienie Ci pisania bardziej solidnego, łatwego w utrzymaniu i wydajnego kodu TypeScript.
Enum TypeScript: Szybkie przypomnienie
Zanim zagłębimy się w alternatywy, pokrótce przypomnijmy tradycyjny enum TypeScript. Enumy umożliwiają deweloperom definiowanie zestawu nazwanych stałych, co zwiększa czytelność kodu i zapobiega rozpraszaniu "magicznych ciągów" czy "magicznych liczb" po całej aplikacji. Występują w dwóch głównych formach: enumy numeryczne i enumy stringowe.
Enumy numeryczne
Domyślnie, enumy TypeScript są numeryczne. Pierwszy element jest inicjalizowany wartością 0, a każdy kolejny element jest automatycznie inkrementowany.
enum Direction {
Up,
Down,
Left,
Right,
}
let currentDirection: Direction = Direction.Up;
console.log(currentDirection); // Outputs: 0
console.log(Direction.Left); // Outputs: 2
Możesz również ręcznie inicjalizować numeryczne elementy enum:
enum StatusCode {
Success = 200,
NotFound = 404,
ServerError = 500,
}
let status: StatusCode = StatusCode.NotFound;
console.log(status); // Outputs: 404
Szczególną cechą enumów numerycznych jest odwrotne mapowanie. W czasie wykonania, enum numeryczny kompiluje się do obiektu JavaScript, który mapuje zarówno nazwy do wartości, jak i wartości z powrotem do nazw.
enum UserRole {
Admin = 1,
Editor,
Viewer,
}
console.log(UserRole[1]); // Outputs: "Admin"
console.log(UserRole.Editor); // Outputs: 2
console.log(UserRole[2]); // Outputs: "Editor"
/*
Compiles to JavaScript:
var UserRole;
(function (UserRole) {
UserRole[UserRole["Admin"] = 1] = "Admin";
UserRole[UserRole["Editor"] = 2] = "Editor";
UserRole[UserRole["Viewer"] = 3] = "Viewer";
})(UserRole || (UserRole = {}));
*/
Enumy stringowe
Enumy stringowe są często preferowane ze względu na ich czytelność w czasie wykonania, ponieważ nie polegają na automatycznie inkrementujących się liczbach. Każdy element musi być inicjalizowany literałem stringowym.
enum UserPermission {
Read = "READ_PERMISSION",
Write = "WRITE_PERMISSION",
Delete = "DELETE_PERMISSION",
}
let permission: UserPermission = UserPermission.Write;
console.log(permission); // Outputs: "WRITE_PERMISSION"
Enumy stringowe nie posiadają odwrotnego mapowania, co jest ogólnie korzystne dla unikania nieoczekiwanego zachowania w czasie wykonania i redukowania generowanego kodu JavaScript.
Kluczowe uwagi i potencjalne pułapki enumów
Chociaż enumy oferują wygodę, wiążą się z pewnymi cechami, które wymagają starannego rozważenia:
- Obiekty w czasie wykonania: Zarówno enumy numeryczne, jak i stringowe generują obiekty JavaScript w czasie wykonania. Oznacza to, że przyczyniają się do rozmiaru pakietu Twojej aplikacji, nawet jeśli używasz ich tylko do sprawdzania typów. W przypadku małych projektów może to być pomijalne, ale w aplikacjach na dużą skalę z wieloma enumami może się to kumulować.
- Brak Tree-Shakingu: Ponieważ enumy są obiektami czasu wykonania, często nie są skutecznie usuwane przez nowoczesne bundlery, takie jak Webpack czy Rollup. Jeśli zdefiniujesz enum, ale użyjesz tylko jednego lub dwóch jego elementów, cały obiekt enum może nadal być dołączony do Twojego końcowego pakietu. Może to prowadzić do większych niż to konieczne rozmiarów plików.
- Odwrotne mapowanie (enumy numeryczne): Funkcja odwrotnego mapowania enumów numerycznych, choć czasami przydatna, może być również źródłem zamieszania i nieoczekiwanego zachowania. Dodaje ona dodatkowy kod do wyjścia JavaScript i może nie zawsze być pożądaną funkcjonalnością. Na przykład, serializacja enumów numerycznych może czasami prowadzić do przechowywania tylko liczby, co może nie być tak opisowe jak string.
- Narzut transpilacji: Kompilacja enumów do obiektów JavaScript dodaje niewielki narzut do procesu budowania w porównaniu do prostego definiowania zmiennych stałych.
- Ograniczona iteracja: Bezpośrednia iteracja po wartościach enum może być nietrywialna, zwłaszcza w przypadku enumów numerycznych ze względu na odwrotne mapowanie. Często potrzebujesz funkcji pomocniczych lub specyficznych pętli, aby uzyskać tylko pożądane wartości.
Powyższe punkty podkreślają, dlaczego wiele globalnych zespołów deweloperskich, zwłaszcza tych skoncentrowanych na wydajności i rozmiarze pakietu, poszukuje alternatyw, które zapewniają podobne bezpieczeństwo typów bez narzutu środowiska wykonawczego lub innych złożoności.
Alternatywa 1: Typy unijne z literałami
Jedną z najbardziej prostych i potężnych alternatyw dla enumów w TypeScript jest użycie Typów Unijnych z Literałami Stringowymi lub Numerycznymi. Podejście to wykorzystuje solidny system typów TypeScript do definiowania zestawu specyficznych, dozwolonych wartości w czasie kompilacji, bez wprowadzania żadnych nowych konstrukcji w czasie wykonania.
Czym są typy unijne?
Typ unijny opisuje wartość, która może być jednym z kilku typów. Na przykład, string | number oznacza, że zmienna może przechowywać albo string, albo liczbę. W połączeniu z typami literałowymi (np. "success", 404), możesz zdefiniować typ, który może przechowywać tylko określony zestaw predefiniowanych wartości.
Praktyczny przykład: Definiowanie statusów za pomocą typów unijnych
Rozważmy typowy scenariusz: definiowanie zestawu możliwych statusów dla zadania przetwarzania danych lub konta użytkownika. Z typami unijnymi wygląda to czysto i zwięźle:
type JobStatus = "PENDING" | "IN_PROGRESS" | "COMPLETED" | "FAILED";
function processJob(status: JobStatus): void {
if (status === "COMPLETED") {
console.log("Job finished successfully.");
} else if (status === "FAILED") {
console.log("Job encountered an error.");
} else {
console.log(`Job is currently ${status}.`);
}
}
let currentJobStatus: JobStatus = "IN_PROGRESS";
processJob(currentJobStatus);
// This would result in a compile-time error:
// let invalidStatus: JobStatus = "CANCELLED"; // Error: Type '"CANCELLED"' is not assignable to type 'JobStatus'.
Dla wartości numerycznych wzorzec jest identyczny:
type HttpCode = 200 | 400 | 404 | 500;
function handleResponse(code: HttpCode): void {
if (code === 200) {
console.log("Operation successful.");
} else if (code === 404) {
console.log("Resource not found.");
}
}
let responseStatus: HttpCode = 200;
handleResponse(responseStatus);
Zauważ, jak definiujemy tutaj alias type. Jest to wyłącznie konstrukcja czasu kompilacji. Po skompilowaniu do JavaScriptu, JobStatus po prostu znika, a literały stringowe/numeryczne są używane bezpośrednio.
Korzyści z typów unijnych z literałami
To podejście oferuje kilka przekonujących zalet:
- Wyłącznie w czasie kompilacji: Typy unijne są całkowicie usuwane podczas kompilacji. Nie generują żadnego kodu JavaScript w czasie wykonania, co prowadzi do mniejszych rozmiarów pakietów i szybszego uruchamiania aplikacji. Jest to znacząca zaleta dla aplikacji krytycznych pod względem wydajności i tych wdrażanych globalnie, gdzie każdy kilobajt ma znaczenie.
- Doskonałe bezpieczeństwo typów: TypeScript rygorystycznie sprawdza przypisania do zdefiniowanych typów literałowych, zapewniając silne gwarancje, że używane są tylko prawidłowe wartości. Zapobiega to powszechnym błędom związanym z literówkami lub nieprawidłowymi wartościami.
- Optymalny Tree-Shaking: Ponieważ nie ma obiektu czasu wykonania, typy unijne z natury wspierają tree-shaking. Twój bundler zawiera tylko rzeczywiste literały stringowe lub numeryczne, których używasz, a nie cały obiekt.
- Czytelność: Dla ustalonego zestawu prostych, odrębnych wartości, definicja typu jest często bardzo jasna i łatwa do zrozumienia.
- Prostota: Nie są wprowadzane żadne nowe konstrukcje językowe ani złożone artefakty kompilacji. To tylko wykorzystanie fundamentalnych cech typów TypeScript.
- Bezpośredni dostęp do wartości: Pracujesz bezpośrednio z wartościami stringowymi lub numerycznymi, co upraszcza serializację i deserializację, zwłaszcza podczas interakcji z API lub bazami danych, które oczekują konkretnych identyfikatorów stringowych.
Wady typów unijnych z literałami
Choć potężne, typy unijne mają również pewne ograniczenia:
- Powtarzanie dla powiązanych danych: Jeśli musisz powiązać dodatkowe dane lub metadane z każdym elementem "enum" (np. etykietę wyświetlania, ikonę, kolor), nie możesz tego zrobić bezpośrednio w definicji typu unijnego. Zazwyczaj potrzebowałbyś osobnego obiektu mapującego.
- Brak bezpośredniej iteracji wszystkich wartości: Nie ma wbudowanego sposobu, aby uzyskać tablicę wszystkich możliwych wartości z typu unijnego w czasie wykonania. Na przykład, nie możesz łatwo uzyskać
["PENDING", "IN_PROGRESS", "COMPLETED", "FAILED"]bezpośrednio zJobStatus. Często wymaga to utrzymywania osobnej tablicy wartości, jeśli musisz je wyświetlić w interfejsie użytkownika (np. w menu rozwijanym). - Mniej scentralizowane: Jeśli zestaw wartości jest potrzebny zarówno jako typ, jak i jako tablica wartości w czasie wykonania, możesz znaleźć się w sytuacji, w której definiujesz listę dwukrotnie (raz jako typ, raz jako tablicę czasu wykonania), co może wprowadzić potencjał do desynchronizacji.
Pomimo tych wad, w wielu scenariuszach typy unijne stanowią czyste, wydajne i bezpieczne typowo rozwiązanie, które dobrze współgra z nowoczesnymi praktykami programowania w JavaScript.
Alternatywa 2: Asercje stałych (as const)
Asercja as const, wprowadzona w TypeScript 3.4, to kolejne niezwykle potężne narzędzie, które oferuje doskonałą alternatywę dla enumów, zwłaszcza gdy potrzebujesz obiektu czasu wykonania i solidnego wnioskowania typów. Pozwala TypeScriptowi na wnioskowanie najwęższego możliwego typu dla wyrażeń literałowych.
Czym są asercje stałych?
Kiedy stosujesz as const do zmiennej, tablicy lub literału obiektu, TypeScript traktuje wszystkie właściwości w tym literale jako readonly i wnioskuje ich typy literałowe zamiast szerszych typów (np. "foo" zamiast string, 123 zamiast number). Umożliwia to wyprowadzanie bardzo specyficznych typów unijnych ze struktur danych czasu wykonania.
Praktyczny przykład: Tworzenie obiektu "pseudo-enum" za pomocą as const
Wróćmy do naszego przykładu statusu zadania. Dzięki as const możemy zdefiniować jedno źródło prawdy dla naszych statusów, które działa zarówno jako obiekt czasu wykonania, jak i podstawa do definicji typów.
const JobStatuses = {
PENDING: "PENDING",
IN_PROGRESS: "IN_PROGRESS",
COMPLETED: "COMPLETED",
FAILED: "FAILED",
} as const;
// JobStatuses.PENDING is now inferred as type "PENDING" (not just string)
// JobStatuses is inferred as type {
// readonly PENDING: "PENDING";
// readonly IN_PROGRESS: "IN_PROGRESS";
// readonly COMPLETED: "COMPLETED";
// readonly FAILED: "FAILED";
// }
W tym momencie JobStatuses jest obiektem JavaScript w czasie wykonania, tak jak zwykły enum. Jednak jego wnioskowanie typów jest znacznie bardziej precyzyjne.
Łączenie z typeof i keyof dla typów unijnych
Prawdziwa moc ujawnia się, gdy połączymy as const z operatorami TypeScript typeof i keyof, aby wyprowadzić typ unijny z wartości lub kluczy obiektu.
const JobStatuses = {
PENDING: "PENDING",
IN_PROGRESS: "IN_PROGRESS",
COMPLETED: "COMPLETED",
FAILED: "FAILED",
} as const;
// Type representing the keys (e.g., "PENDING" | "IN_PROGRESS" | ...)
type JobStatusKeys = keyof typeof JobStatuses;
// Type representing the values (e.g., "PENDING" | "IN_PROGRESS" | ...)
type JobStatusValues = typeof JobStatuses[keyof typeof JobStatuses];
function processJobWithConstAssertion(status: JobStatusValues): void {
if (status === JobStatuses.COMPLETED) {
console.log("Job finished successfully.");
} else if (status === JobStatuses.FAILED) {
console.log("Job encountered an error.");
} else {
console.log(`Job is currently ${status}.`);
}
}
let currentJobStatusFromObject: JobStatusValues = JobStatuses.IN_PROGRESS;
processJobWithConstAssertion(currentJobStatusFromObject);
// This would result in a compile-time error:
// let invalidStatusFromObject: JobStatusValues = "CANCELLED"; // Error!
Korzyści z asercji stałych z pochodnymi typami unijnymi
- Jedno źródło prawdy: Definiujesz swoje stałe raz w zwykłym obiekcie JavaScript i wyprowadzasz z niego zarówno dostęp w czasie wykonania, jak i typy w czasie kompilacji. To znacznie redukuje duplikację i poprawia utrzymywalność w różnych zespołach deweloperskich.
- Bezpieczeństwo typów: Podobnie jak w przypadku czystych typów unijnych, uzyskujesz doskonałe bezpieczeństwo typów, zapewniając, że używane są tylko predefiniowane wartości.
- Iterowalność w czasie wykonania: Ponieważ
JobStatusesjest zwykłym obiektem JavaScript, możesz łatwo iterować po jego kluczach lub wartościach za pomocą standardowych metod JavaScript, takich jakObject.keys(),Object.values()lubObject.entries(). Jest to nieocenione dla dynamicznych interfejsów użytkownika (np. wypełniania list rozwijanych) lub logowania. - Powiązane dane: Ten wzorzec naturalnie wspiera powiązywanie dodatkowych danych z każdym elementem "enum".
- Lepszy potencjał Tree-Shakingu (w porównaniu do enumów): Chociaż
as consttworzy obiekt czasu wykonania, jest to standardowy obiekt JavaScript. Nowoczesne bundlery są zazwyczaj bardziej skuteczne w usuwaniu nieużywanych właściwości tego obiektu, a nawet całych obiektów, jeśli nie są one odwoływane, w porównaniu do danych wyjściowych kompilacji enumów TypeScript. Jednakże, jeśli obiekt jest duży i używane są tylko niektóre właściwości, cały obiekt może nadal być dołączony, jeśli zostanie zaimportowany w sposób, który uniemożliwia granularny tree-shaking. - Elastyczność: Możesz definiować wartości, które nie są tylko stringami lub liczbami, ale w razie potrzeby bardziej złożonymi obiektami, co czyni ten wzorzec wysoce elastycznym.
const FileOperations = {
UPLOAD: {
label: "Upload File",
icon: "upload-icon.svg",
permission: "can_upload"
},
DOWNLOAD: {
label: "Download File",
icon: "download-icon.svg",
permission: "can_download"
},
DELETE: {
label: "Delete File",
icon: "delete-icon.svg",
permission: "can_delete"
},
} as const;
type FileOperationType = keyof typeof FileOperations; // "UPLOAD" | "DOWNLOAD" | "DELETE"
type FileOperationDetail = typeof FileOperations[keyof typeof FileOperations]; // { label: string; icon: string; permission: string; }
function performOperation(opType: FileOperationType) {
const details = FileOperations[opType];
console.log(`Performing: ${details.label} (Permission: ${details.permission})`);
}
performOperation("UPLOAD");
Wady asercji stałych
- Obecność obiektu w czasie wykonania: W przeciwieństwie do czystych typów unijnych, to podejście nadal tworzy obiekt JavaScript w czasie wykonania. Chociaż jest to standardowy obiekt i często lepszy dla tree-shakingu niż enumy, nie jest on całkowicie usuwany.
- Nieco bardziej rozwlekła definicja typu: Wyprowadzenie typu unijnego (
keyof typeof ...lubtypeof ...[keyof typeof ...]) wymaga nieco więcej składni niż po prostu wymienienie literałów dla typu unijnego. - Potencjalne niewłaściwe użycie: Jeśli nie będzie używany ostrożnie, bardzo duży obiekt
as constmoże nadal znacząco przyczyniać się do rozmiaru pakietu, jeśli jego zawartość nie zostanie skutecznie usunięta przez tree-shaking przez granice modułów.
W scenariuszach, w których potrzebujesz zarówno solidnego sprawdzania typów w czasie kompilacji, jak i kolekcji wartości w czasie wykonania, które można iterować lub które dostarczają powiązane dane, as const jest często preferowanym wyborem wśród deweloperów TypeScript na całym świecie.
Porównanie alternatyw: Kiedy używać czego?
Wybór między typami unijnymi a asercjami stałych w dużej mierze zależy od Twoich konkretnych wymagań dotyczących obecności w czasie wykonania, iterowalności oraz tego, czy potrzebujesz powiązać dodatkowe dane ze swoimi stałymi. Przyjrzyjmy się czynnikom decyzyjnym.
Prostota kontra Solidność
- Typy unijne: Oferują najwyższą prostotę, gdy potrzebujesz tylko bezpiecznego typowo zestawu odrębnych wartości stringowych lub numerycznych w czasie kompilacji. Są najlżejszą opcją.
- Asercje stałych: Zapewniają bardziej solidny wzorzec, gdy potrzebujesz zarówno bezpieczeństwa typów w czasie kompilacji, jak i obiektu czasu wykonania, który może być zapytany, iterowany lub rozszerzony o dodatkowe metadane. Początkowa konfiguracja jest nieco bardziej szczegółowa, ale opłaca się w postaci funkcjonalności.
Obecność w czasie wykonania kontra w czasie kompilacji
- Typy unijne: Są czysto konstrukcjami czasu kompilacji. Absolutnie nie generują kodu JavaScript. Jest to idealne rozwiązanie dla aplikacji, gdzie minimalizacja rozmiaru pakietu jest najważniejsza, a same wartości są wystarczające, bez konieczności dostępu do nich jako obiektu w czasie wykonania.
- Asercje stałych: Generują zwykły obiekt JavaScript w czasie wykonania. Ten obiekt jest dostępny i użyteczny w Twoim kodzie JavaScript. Chociaż zwiększa rozmiar pakietu, jest on ogólnie bardziej wydajny niż enumy TypeScript i lepszy kandydat do tree-shakingu.
Wymagania dotyczące iterowalności
- Typy unijne: Nie oferują bezpośredniego sposobu iteracji po wszystkich możliwych wartościach w czasie wykonania. Jeśli potrzebujesz wypełnić menu rozwijane lub wyświetlić wszystkie opcje, będziesz musiał zdefiniować osobną tablicę tych wartości, co potencjalnie prowadzi do duplikacji.
- Asercje stałych: W tym przypadku są doskonałe. Ponieważ pracujesz ze standardowym obiektem JavaScript, możesz łatwo użyć
Object.keys(),Object.values()lubObject.entries(), aby uzyskać tablicę odpowiednio kluczy, wartości lub par klucz-wartość. To sprawia, że są idealne do dynamicznych interfejsów użytkownika (np. wypełniania list rozwijanych) lub każdego scenariusza wymagającego wyliczenia w czasie wykonania.
const PaymentMethods = {
CREDIT_CARD: "Credit Card",
PAYPAL: "PayPal",
BANK_TRANSFER: "Bank Transfer",
} as const;
type PaymentMethodType = keyof typeof PaymentMethods;
// Get all keys (e.g., for internal logic)
const methodKeys = Object.keys(PaymentMethods) as PaymentMethodType[];
console.log(methodKeys); // ["CREDIT_CARD", "PAYPAL", "BANK_TRANSFER"]
// Get all values (e.g., for display in a dropdown)
const methodLabels = Object.values(PaymentMethods);
console.log(methodLabels); // ["Credit Card", "PayPal", "Bank Transfer"]
// Get key-value pairs (e.g., for mapping)
const methodEntries = Object.entries(PaymentMethods);
console.log(methodEntries); // [["CREDIT_CARD", "Credit Card"], ...]
Implikacje Tree-Shakingu
- Typy unijne: Są z natury podatne na tree-shaking, ponieważ istnieją tylko w czasie kompilacji.
- Asercje stałych: Chociaż tworzą obiekt w czasie wykonania, nowoczesne bundlery często mogą skuteczniej usuwać nieużywane właściwości tego obiektu niż w przypadku generowanych przez TypeScript obiektów enum. Jednakże, jeśli cały obiekt zostanie zaimportowany i odwołany, prawdopodobnie zostanie on włączony. Staranny projekt modułów może pomóc.
Najlepsze praktyki i podejścia hybrydowe
Nie zawsze jest to sytuacja typu "albo/albo". Często najlepszym rozwiązaniem jest podejście hybrydowe, szczególnie w dużych, umiędzynarodowionych aplikacjach:
- Dla prostych, czysto wewnętrznych flag lub identyfikatorów, które nigdy nie muszą być iterowane ani posiadać powiązanych danych, Typy unijne są ogólnie najbardziej wydajnym i najczystszym wyborem.
- Dla zestawów stałych, które muszą być iterowane, wyświetlane w interfejsach użytkownika lub posiadają bogate powiązane metadane (takie jak etykiety, ikony lub uprawnienia), wzorzec Asercji stałych jest lepszy.
- Łączenie dla czytelności i lokalizacji: Wiele zespołów używa
as constdla wewnętrznych identyfikatorów, a następnie wyprowadza zlokalizowane etykiety wyświetlania z oddzielnego systemu internacjonalizacji (i18n).
// src/constants/order-status.ts
const OrderStatuses = {
PENDING: "PENDING",
PROCESSING: "PROCESSING",
SHIPPED: "SHIPPED",
DELIVERED: "DELIVERED",
CANCELLED: "CANCELLED",
} as const;
type OrderStatus = typeof OrderStatuses[keyof typeof OrderStatuses];
export { OrderStatuses, type OrderStatus };
// src/i18n/en.json
{
"orderStatus": {
"PENDING": "Pending Confirmation",
"PROCESSING": "Processing Order",
"SHIPPED": "Shipped",
"DELIVERED": "Delivered",
"CANCELLED": "Cancelled"
}
}
// src/i18n/locales/es.json
{
"productCategories": {
"ELECTRONICS": "Electrónica",
"APPAREL": "Ropa y Akcesoria",
"HOME_GOODS": "Artículos para el hogar",
"BOOKS": "Libros"
}
}
// src/components/OrderStatusDisplay.tsx
import { OrderStatuses, type OrderStatus } from "../constants/order-status";
import { useTranslation } from "react-i18next"; // Example i18n library
interface OrderStatusDisplayProps {
status: OrderStatus;
}
function OrderStatusDisplay({ status }: OrderStatusDisplayProps) {
const { t } = useTranslation();
const displayLabel = t(`orderStatus.${status}`);
return <span>Status: {displayLabel}</span>;
}
// Usage:
// <OrderStatusDisplay status={OrderStatuses.DELIVERED} />
To hybrydowe podejście wykorzystuje bezpieczeństwo typów i iterowalność w czasie wykonania as const, jednocześnie utrzymując zlokalizowane ciągi wyświetlania oddzielnie i łatwo zarządzalne, co jest kluczowym aspektem dla globalnych aplikacji.
Zaawansowane wzorce i uwagi
Poza podstawowym zastosowaniem, zarówno typy unijne, jak i asercje stałych mogą być integrowane w bardziej zaawansowane wzorce w celu dalszego zwiększenia jakości i utrzymywalności kodu.
Używanie strażników typów z typami unijnymi
Podczas pracy z typami unijnymi, zwłaszcza gdy unia obejmuje różnorodne typy (nie tylko literały), strażnicy typów stają się niezbędni do zawężania typów. W przypadku literałowych typów unijnych, unie rozróżnialne oferują ogromną moc.
type SuccessEvent = { type: "SUCCESS"; data: any; };
type ErrorEvent = { type: "ERROR"; message: string; code: number; };
type SystemEvent = SuccessEvent | ErrorEvent;
function handleSystemEvent(event: SystemEvent) {
if (event.type === "SUCCESS") {
console.log("Data received:", event.data);
// event is now narrowed to SuccessEvent
} else {
console.log("Error occurred:", event.message, "Code:", event.code);
// event is now narrowed to ErrorEvent
}
}
handleSystemEvent({ type: "SUCCESS", data: { user: "Alice" } });
handleSystemEvent({ type: "ERROR", message: "Network failure", code: 503 });
Ten wzorzec, często nazywany "uniami rozróżnialnymi", jest niezwykle solidny i bezpieczny typowo, zapewniając gwarancje w czasie kompilacji dotyczące struktury Twoich danych na podstawie wspólnej właściwości literałowej (dyskryminatora).
Object.values() z as const i asercjami typów
Podczas używania wzorca as const, metoda Object.values() może być bardzo użyteczna. Jednak domyślne wnioskowanie typu przez TypeScript dla Object.values() może być szersze niż pożądane (np. string[] zamiast konkretnej unii literałów). W celu zachowania ściskości może być potrzebna asercja typu.
const Statuses = {
ACTIVE: "Active",
INACTIVE: "Inactive",
PENDING: "Pending",
} as const;
type StatusValue = typeof Statuses[keyof typeof Statuses]; // "Active" | "Inactive" | "Pending"
// Object.values(Statuses) is inferred as (string | "Active" | "Inactive" | "Pending")[]
// We can assert it more narrowly if needed:
const allStatusValues: StatusValue[] = Object.values(Statuses);
console.log(allStatusValues); // ["Active", "Inactive", "Pending"]
// For a dropdown, you might pair values with labels if they differ
const statusOptions = Object.entries(Statuses).map(([key, value]) => ({
value: key, // Use the key as the actual identifier
label: value // Use the value as the display label
}));
console.log(statusOptions);
/*
[
{ value: "ACTIVE", label: "Active" },
{ value: "INACTIVE", label: "Inactive" },
{ value: "PENDING", label: "Pending" }
]
*/
Internacjonalizacja (i18n) i zlokalizowane etykiety
Dla globalnych aplikacji zarządzanie zlokalizowanymi ciągami znaków jest kluczowe. Chociaż enumy TypeScript i ich alternatywy dostarczają wewnętrznych identyfikatorów, etykiety wyświetlania często muszą być oddzielone dla i18n. Wzorzec as const pięknie uzupełnia systemy i18n.
Definiujesz swoje wewnętrzne, niezmienne identyfikatory za pomocą as const. Te identyfikatory są spójne we wszystkich lokalizacjach i służą jako klucze dla Twoich plików tłumaczeń. Rzeczywiste ciągi wyświetlania są następnie pobierane z biblioteki i18n (np. react-i18next, vue-i18n, FormatJS) na podstawie wybranego przez użytkownika języka.
// app/features/product/constants.ts
export const ProductCategories = {
ELECTRONICS: "ELECTRONICS",
APPAREL: "APPAREL",
HOME_GOODS: "HOME_GOODS",
BOOKS: "BOOKS",
} as const;
export type ProductCategory = typeof ProductCategories[keyof typeof ProductCategories];
// app/i18n/locales/en.json
{
"productCategories": {
"ELECTRONICS": "Electronics",
"APPAREL": "Apparel & Accessories",
"HOME_GOODS": "Home Goods",
"BOOKS": "Books"
}
}
// app/i18n/locales/es.json
{
"productCategories": {
"ELECTRONICS": "Electrónica",
"APPAREL": "Ropa y Akcesoria",
"HOME_GOODS": "Artículos para el hogar",
"BOOKS": "Libros"
}
}
// app/components/ProductCategorySelector.tsx
import { ProductCategories, type ProductCategory } from "../features/product/constants";
import { useTranslation } from "react-i18next";
function ProductCategorySelector() {
const { t } = useTranslation();
return (
<select>
{Object.values(ProductCategories).map(categoryKey => (
<option key={categoryKey} value={categoryKey}>
{t(`productCategories.${categoryKey}`)}
</option>
))}
</select>
);
}
To rozdzielenie odpowiedzialności jest kluczowe dla skalowalnych, globalnych aplikacji. Typy TypeScript zapewniają, że zawsze używasz prawidłowych kluczy, a system i18n obsługuje warstwę prezentacji w oparciu o lokalizację użytkownika. Pozwala to uniknąć bezpośredniego osadzania ciągów zależnych od języka w logice rdzenia aplikacji, co jest powszechnym antywzorcem w międzynarodowych zespołach.
Podsumowanie: Wzmocnij swoje wybory projektowe w TypeScript
W miarę jak TypeScript ewoluuje i umożliwia deweloperom na całym świecie tworzenie bardziej solidnych i skalowalnych aplikacji, zrozumienie jego niuansowanych funkcji i alternatyw staje się coraz ważniejsze. Chociaż słowo kluczowe enum w TypeScript oferuje wygodny sposób definiowania nazwanych stałych, jego ślad w czasie wykonania, ograniczenia tree-shakingu i złożoności odwrotnego mapowania często sprawiają, że nowoczesne alternatywy są bardziej atrakcyjne dla projektów wrażliwych na wydajność lub na dużą skalę.
Typy unijne z literałami string/numerycznymi wyróżniają się jako najmniej zasobochłonne i najbardziej skoncentrowane na czasie kompilacji rozwiązanie. Zapewniają bezkompromisowe bezpieczeństwo typów bez generowania kodu JavaScript w czasie wykonania, co czyni je idealnymi dla scenariuszy, w których minimalny rozmiar pakietu i maksymalny tree-shaking są priorytetem, a wyliczanie w czasie wykonania nie jest problemem.
Z drugiej strony, Asercje stałych (as const) w połączeniu z typeof i keyof oferują bardzo elastyczny i potężny wzorzec. Zapewniają one jedno źródło prawdy dla Twoich stałych, silne bezpieczeństwo typów w czasie kompilacji oraz kluczową możliwość iteracji po wartościach w czasie wykonania. To podejście jest szczególnie dobrze dopasowane do sytuacji, w których musisz powiązać dodatkowe dane ze swoimi stałymi, wypełniać dynamiczne interfejsy użytkownika lub bezproblemowo integrować się z systemami internacjonalizacji.
Starannie rozważając kompromisy – ślad w czasie wykonania, potrzeby iterowalności i złożoność powiązanych danych – możesz podejmować świadome decyzje, które prowadzą do czystszego, bardziej wydajnego i łatwiejszego w utrzymaniu kodu TypeScript. Przyjęcie tych alternatyw to nie tylko pisanie "nowoczesnego" TypeScriptu; to podejmowanie celowych wyborów architektonicznych, które poprawiają wydajność aplikacji, doświadczenie deweloperów i długoterminową trwałość dla globalnej publiczności.
Wzmocnij swój rozwój w TypeScript, wybierając odpowiednie narzędzie do odpowiedniego zadania, wykraczając poza domyślny enum, gdy istnieją lepsze alternatywy.